/* **************************************************************************
 * $OpenLDAP: /com/novell/sasl/client/DirectiveList.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
 *
 * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
 *
 * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
 * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
 * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
 * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
 * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
 * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
 * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
 * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
 ******************************************************************************/
package com.novell.sasl.client;

import org.apache.harmony.javax.security.sasl.SaslException;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * Implements the DirectiveList class whihc will be used by the
 * DigestMD5SaslClient class
 */
class DirectiveList extends Object {
    private static final int STATE_LOOKING_FOR_FIRST_DIRECTIVE = 1;
    private static final int STATE_LOOKING_FOR_DIRECTIVE = 2;
    private static final int STATE_SCANNING_NAME = 3;
    private static final int STATE_LOOKING_FOR_EQUALS = 4;
    private static final int STATE_LOOKING_FOR_VALUE = 5;
    private static final int STATE_LOOKING_FOR_COMMA = 6;
    private static final int STATE_SCANNING_QUOTED_STRING_VALUE = 7;
    private static final int STATE_SCANNING_TOKEN_VALUE = 8;
    private static final int STATE_NO_UTF8_SUPPORT = 9;

    private int m_curPos;
    private int m_errorPos;
    private String m_directives;
    private int m_state;
    private ArrayList m_directiveList;
    private String m_curName;
    private int m_scanStart;

    /**
     * Constructs a new DirectiveList.
     */
    DirectiveList(
            byte[] directives) {
        m_curPos = 0;
        m_state = STATE_LOOKING_FOR_FIRST_DIRECTIVE;
        m_directiveList = new ArrayList(10);
        m_scanStart = 0;
        m_errorPos = -1;
        try {
            m_directives = new String(directives, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            m_state = STATE_NO_UTF8_SUPPORT;
        }
    }

    /**
     * This function takes a US-ASCII character string containing a list of comma
     * separated directives, and parses the string into the individual directives
     * and their values. A directive consists of a token specifying the directive
     * name followed by an equal sign (=) and the directive value. The value is
     * either a token or a quoted string
     *
     * @throws SaslException If an error Occurs
     */
    void parseDirectives() throws SaslException {
        char prevChar;
        char currChar;
        int rc = 0;
        boolean haveQuotedPair = false;
        String currentName = "<no name>";

        if (m_state == STATE_NO_UTF8_SUPPORT)
            throw new SaslException("No UTF-8 support on platform");

        prevChar = 0;

        while (m_curPos < m_directives.length()) {
            currChar = m_directives.charAt(m_curPos);
            switch (m_state) {
                case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
                case STATE_LOOKING_FOR_DIRECTIVE:
                    if (isWhiteSpace(currChar)) {
                        break;
                    } else if (isValidTokenChar(currChar)) {
                        m_scanStart = m_curPos;
                        m_state = STATE_SCANNING_NAME;
                    } else {
                        m_errorPos = m_curPos;
                        throw new SaslException("Parse error: Invalid name character");
                    }
                    break;

                case STATE_SCANNING_NAME:
                    if (isValidTokenChar(currChar)) {
                        break;
                    } else if (isWhiteSpace(currChar)) {
                        currentName = m_directives.substring(m_scanStart, m_curPos);
                        m_state = STATE_LOOKING_FOR_EQUALS;
                    } else if ('=' == currChar) {
                        currentName = m_directives.substring(m_scanStart, m_curPos);
                        m_state = STATE_LOOKING_FOR_VALUE;
                    } else {
                        m_errorPos = m_curPos;
                        throw new SaslException("Parse error: Invalid name character");
                    }
                    break;

                case STATE_LOOKING_FOR_EQUALS:
                    if (isWhiteSpace(currChar)) {
                        break;
                    } else if ('=' == currChar) {
                        m_state = STATE_LOOKING_FOR_VALUE;
                    } else {
                        m_errorPos = m_curPos;
                        throw new SaslException("Parse error: Expected equals sign '='.");
                    }
                    break;

                case STATE_LOOKING_FOR_VALUE:
                    if (isWhiteSpace(currChar)) {
                        break;
                    } else if ('"' == currChar) {
                        m_scanStart = m_curPos + 1; /* don't include the quote */
                        m_state = STATE_SCANNING_QUOTED_STRING_VALUE;
                    } else if (isValidTokenChar(currChar)) {
                        m_scanStart = m_curPos;
                        m_state = STATE_SCANNING_TOKEN_VALUE;
                    } else {
                        m_errorPos = m_curPos;
                        throw new SaslException("Parse error: Unexpected character");
                    }
                    break;

                case STATE_SCANNING_TOKEN_VALUE:
                    if (isValidTokenChar(currChar)) {
                        break;
                    } else if (isWhiteSpace(currChar)) {
                        addDirective(currentName, false);
                        m_state = STATE_LOOKING_FOR_COMMA;
                    } else if (',' == currChar) {
                        addDirective(currentName, false);
                        m_state = STATE_LOOKING_FOR_DIRECTIVE;
                    } else {
                        m_errorPos = m_curPos;
                        throw new SaslException("Parse error: Invalid value character");
                    }
                    break;

                case STATE_SCANNING_QUOTED_STRING_VALUE:
                    if ('\\' == currChar)
                        haveQuotedPair = true;
                    if (('"' == currChar) &&
                            ('\\' != prevChar)) {
                        addDirective(currentName, haveQuotedPair);
                        haveQuotedPair = false;
                        m_state = STATE_LOOKING_FOR_COMMA;
                    }
                    break;

                case STATE_LOOKING_FOR_COMMA:
                    if (isWhiteSpace(currChar))
                        break;
                    else if (currChar == ',')
                        m_state = STATE_LOOKING_FOR_DIRECTIVE;
                    else {
                        m_errorPos = m_curPos;
                        throw new SaslException("Parse error: Expected a comma.");
                    }
                    break;
            }
            if (0 != rc)
                break;
            prevChar = currChar;
            m_curPos++;
        } /* end while loop */


        if (rc == 0) {
            /* check the ending state */
            switch (m_state) {
                case STATE_SCANNING_TOKEN_VALUE:
                    addDirective(currentName, false);
                    break;

                case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
                case STATE_LOOKING_FOR_COMMA:
                    break;

                case STATE_LOOKING_FOR_DIRECTIVE:
                    throw new SaslException("Parse error: Trailing comma.");

                case STATE_SCANNING_NAME:
                case STATE_LOOKING_FOR_EQUALS:
                case STATE_LOOKING_FOR_VALUE:
                    throw new SaslException("Parse error: Missing value.");

                case STATE_SCANNING_QUOTED_STRING_VALUE:
                    throw new SaslException("Parse error: Missing closing quote.");
            }
        }

    }

    /**
     * This function returns TRUE if the character is a valid token character.
     * <p>
     * token          = 1*<any CHAR except CTLs or separators>
     * <p>
     * separators     = "(" | ")" | "<" | ">" | "@"
     * | "," | ";" | ":" | "\" | <">
     * | "/" | "[" | "]" | "?" | "="
     * | "{" | "}" | SP | HT
     * <p>
     * CTL            = <any US-ASCII control character
     * (octets 0 - 31) and DEL (127)>
     * <p>
     * CHAR           = <any US-ASCII character (octets 0 - 127)>
     *
     * @param c character to be tested
     * @return Returns TRUE if the character is a valid token character.
     */
    boolean isValidTokenChar(
            char c) {
        if (((c >= '\u0000') && (c <= '\u0020')) ||
                ((c >= '\u003a') && (c <= '\u0040')) ||
                ((c >= '\u005b') && (c <= '\u005d')) ||
                ('\u002c' == c) ||
                ('\u0025' == c) ||
                ('\u0028' == c) ||
                ('\u0029' == c) ||
                ('\u007b' == c) ||
                ('\u007d' == c) ||
                ('\u007f' == c))
            return false;

        return true;
    }

    /**
     * This function returns TRUE if the character is linear white space (LWS).
     * LWS = [CRLF] 1*( SP | HT )
     *
     * @param c Input charcter to be tested
     * @return Returns TRUE if the character is linear white space (LWS)
     */
    boolean isWhiteSpace(
            char c) {
        if (('\t' == c) ||  // HORIZONTAL TABULATION.
                ('\n' == c) ||  // LINE FEED.
                ('\r' == c) ||  // CARRIAGE RETURN.
                ('\u0020' == c))
            return true;

        return false;
    }

    /**
     * This function creates a directive record and adds it to the list, the
     * value will be added later after it is parsed.
     *
     * @param name           Name
     * @param haveQuotedPair true if quoted pair is there else false
     */
    void addDirective(
            String name,
            boolean haveQuotedPair) {
        String value;
        int inputIndex;
        int valueIndex;
        char valueChar;
        int type;

        if (!haveQuotedPair) {
            value = m_directives.substring(m_scanStart, m_curPos);
        } else { //copy one character at a time skipping backslash excapes.
            StringBuffer valueBuf = new StringBuffer(m_curPos - m_scanStart);
            valueIndex = 0;
            inputIndex = m_scanStart;
            while (inputIndex < m_curPos) {
                if ('\\' == (valueChar = m_directives.charAt(inputIndex)))
                    inputIndex++;
                valueBuf.setCharAt(valueIndex, m_directives.charAt(inputIndex));
                valueIndex++;
                inputIndex++;
            }
            value = new String(valueBuf);
        }

        if (m_state == STATE_SCANNING_QUOTED_STRING_VALUE)
            type = ParsedDirective.QUOTED_STRING_VALUE;
        else
            type = ParsedDirective.TOKEN_VALUE;
        m_directiveList.add(new ParsedDirective(name, value, type));
    }


    /**
     * Returns the List iterator.
     *
     * @return Returns the Iterator Object for the List.
     */
    Iterator getIterator() {
        return m_directiveList.iterator();
    }
}

